/*
 * For legal stuff see the file COPYRIGHT
 */
#import <stdio.h>
#import <time.h>
#import <bsd/libc.h>
#import <sys/file.h>
#import <sys/types.h>
#import <sys/stat.h>
#import <objc/NXBundle.h>
#import <appkit/appkit.h>
#import "Controller.h"
#import "Invoice.h"
#import "Session.h"
#import "Expense.h"
#import "Preferences.h"
#import "createPath.h"

#define EXPENSE			"Expense"
#define DETAIL			"Detail"
#define INVOICE			"Invoice"
#define TEMPLATE_TYPE		"rtf"

#define MONTH_SWITCH_DAY	20


@interface Invoice(PRIVATE)
- (void)copyTemplateIfNeeded:(const char *)template;
- (FILE *)openTemplate:(const char *)template;
- (FILE *)openOutput:(const char *)path;
- (void)filterExpenses:(FILE *)in to:(FILE *)out;
- (void)filterDetail:(FILE *)in to:(FILE *)out;
- (void)filter:(FILE *)in to:(FILE *)out;
- (const char *)translate:(const char *)token;
- (BOOL)filterBuffer:(char *)ptr to:(FILE *)out
 	    match:(const char *)target;
- (const char *)templatePath:(const char *)template;
- (const char *)createFilename:(const char *)type;
- (int)nextInvoiceNumber;
- generateInvoice;
- generateDetail;
- generateExpenses;
- (int)likelyMonth:(struct tm *)tm;

- (const char *)number;		/* invoice number */
- (const char *)today;		/* today's date */
- (const char *)thisMonth;	/* the likely current invoicing month */
- (const char *)contact;	/* person at client */
- (const char *)client;		/* client name */
- (const char *)street;		/* street address */
- (const char *)city;
- (const char *)state;
- (const char *)zip;
- (const char *)enddate;	/* end of invoice period */
- (const char *)startdate;	/* start of invoice period */
- (const char *)billings;	/* total hourly billings */
- (const char *)expenses;	/* total expenses */
- (const char *)total;		/* billings + expenses */
- (const char *)hours;		/* total hours worked for invoice period */
- (const char *)rate;		/* hourly rate */
- (const char *)date;		/* date of a session or expense */
- (const char *)length;		/* length of a session (in hours) */
- (const char *)amount;		/* expense amount */
- (const char *)description;	/* description of session task or expense */

- (const char *)myName;		/* consultant's personal information, from prefs */
- (const char *)myCompany;
- (const char *)myStreet;
- (const char *)myCity;
- (const char *)myState;
- (const char *)myZip;
- (const char *)myPhone;
- (const char *)myFax;
- (const char *)myEmail;
@end


@implementation Invoice


- initTemplateDir:(const char *)dir
{
  [super init];

  templateDir = NXCopyStringBuffer(dir);

  if ( createPath( templateDir, DIRMODE ) != PathCreationOk ) {
    NXRunAlertPanel( [NXApp name], "Cannot create path `%s'",
		    "Harumph.", NULL, NULL, templateDir );
    return nil;
  }

  /*
   * If the templates are not found in the templateDir,
   * copy them there from the app wrapper.
   */
  [self copyTemplateIfNeeded:EXPENSE];
  [self copyTemplateIfNeeded:INVOICE];
  [self copyTemplateIfNeeded:DETAIL];

  return self;
}

- free
{
  if ( templateDir ) {
    free(templateDir);
    templateDir = NULL;
  }
  return [super free];
}

- (BOOL)editTemplate:(const char *)template
{
  const char *path = [self templatePath:template];
  return [[Application workspace] openFile:path];
}

- (void)generate:(List *)clientList
{
  int i, count = [clientList count];
  int sessionCount, expenseCount;

  for ( i = 0; i < count; i++ ) {

    info = [clientList objectAt:i];
    sessionCount = [info sessionCount];
    expenseCount = [info expenseCount];

    /* Don't generate an invoice if no work was done */
    if ( sessionCount == 0 && expenseCount == 0 )
      continue;

    /* get (and increment) the invoice number */ 
    number = [self nextInvoiceNumber];

    [self generateInvoice];

    if ( sessionCount > 0 )
      [self generateDetail];

    if ( expenseCount > 0 )
      [self generateExpenses];
  }
}

@end


@implementation Invoice(PRIVATE)

static char *Tokens[] = {
  "number",
  "today",
  "contact",
  "client",
  "street",
  "city",
  "state",
  "zip",
  "startdate",
  "enddate",
  "billings",
  "expenses",
  "total",
  "hours",
  "rate",
  "date",
  "length",
  "amount",
  "description",
  "myName",
  "myCompany",
  "myStreet",
  "myCity",
  "myState",
  "myZip",
  "myPhone",
  "myFax",
  "myEmail",
  0,
};

#define	START		'['
#define	END		']'
#define MAX_LINE	500
#define MAX_TOKEN	20

/*
 * Determine the value of the token, or return NULL if
 * not a known token.
 */
- (const char *)translate:(const char *)token
{
  char **ptr;

  for ( ptr = Tokens; *ptr; ptr++ ) {
    if ( strcmp( *ptr, token ) == 0 ) {
      SEL sel = sel_getUid(token);

      if ( [self respondsTo:sel] )
	return (const char *)[self perform:sel];
      else
	return NULL;		/* found string, but don't respond */
    }
  }
  return NULL;
}

/*
 * Translate the line pointed to by 'ptr', writing the result to the
 * stream 'out'.  Return 1 if the given token occurred in this line.
 */
- (BOOL)filterBuffer:(char *)ptr to:(FILE *)output
 	    match:(const char *)target
{
  char c, *tok, token[MAX_TOKEN + 1];
  const char *value;
  int count;
  BOOL matched = NO;

  for ( ; *ptr ; ptr++ ) {
    if ( *ptr == START ) {

      for (count = 0, tok = token;
	   count < MAX_TOKEN && (c = *++ptr) && NXIsAlpha(c);
	   count++ )
	*tok++ = c;

      *tok = '\0';

      if ( c == END ) {					    /* got a token */
	if ( (value = [self translate:token]) == NULL )	    /* it was unknown */
	  fprintf(output, "%c%s%c", START, token, END);	    /* pass it thru */
	else {
	  fprintf(output, "%s", value);			    /* use its value */
	  if ( target && strcmp( token, target ) == 0 )
	    matched = YES;
	}
      } else if ( !NXIsAlpha(c) )			    /* bad character */
	fprintf(output, "%c%s%c", START, token, c);	    /* pass it thru */
      else						    /* it was too long */
	fprintf(output, "%c%s", START, token);		    /* pass it thru */
    } else
      fputc( *ptr, output );
  }
  return matched;
}

/*
 * Straight translation - used to generate invoices.
 */
- (void)filter:(FILE *)input to:(FILE *)output
{
  char buf[MAX_LINE + 1];

  while ( fgets( buf, MAX_LINE, input ) )
    [self filterBuffer:buf to:output match:NULL];
}

/*
 * Expense generator - loops over Expense records when it
 * finds the {description} token in a line.
 */
- (void)filterExpenses:(FILE *)input to:(FILE *)output
{
  char buf[MAX_LINE + 1];

  while ( fgets( buf, MAX_LINE, input ) ) {
    /*
     * When we find {description}, treat the current buf as
     * the template for all expenses.
     */
    if ( [self filterBuffer:buf to:output match:"description"] ) {
      int i, count = [info expenseCount];

      for ( i = 0; i < count; i++ ) {
	expense = (Expense *)[info expenseAt:i];
	[self filterBuffer:buf to:output match:NULL];
      }
      expense = nil;
    }
  }
}

/*
 * Detail generator - loops over Session records when it
 * finds the {description} token in a line.
 */
- (void)filterDetail:(FILE *)input to:(FILE *)output
{
  char buf[MAX_LINE + 1];

  while ( fgets( buf, MAX_LINE, input ) ) {
    /*
     * When we find {description}, treat the current buf as
     * the template for all sessions.
     */
    if ( [self filterBuffer:buf to:output match:"description"] ) {
      int i, count = [info sessionCount];
      
      for ( i = 0; i < count; i++ ) {
	session = (Session *)[info sessionAt:i];
	[self filterBuffer:buf to:output match:NULL];
      }
      session = nil;
    }
  }
}

/*
 * Intuit the most likely month that this invoice is for. If it is before
 * the 20th day of the month, assume it is for the prior month, otherwise
 * assume it is for the current month.
 */
- (int)likelyMonth:(struct tm *)tm
{
  int month = tm->tm_mon;

  if ( tm->tm_mday < MONTH_SWITCH_DAY )
    month = ( month == 0 ? 11 : month - 1 );	/* special for January */

  return month;
}

/*
 * Return the (likely) current invoicing month.
 */
- (const char *)thisMonth
{
  time_t now;
  struct tm *tm;
  static char *months[] = {
    "jan", "feb", "mar", "apr",
    "may", "jun", "jul", "aug", "sep",
    "oct", "nov", "dec"
  };

  time(&now);
  tm = localtime(&now);

  return months[[self likelyMonth:tm]];
}

/***********************************************************************
 * 			Token Translation Methods
 ***********************************************************************/
- (const char *)number
{
  static char buf[20];
  sprintf( buf, "%d", number );
  return buf;
}

/*
 * Return today's date as a string
 */
- (const char *)today
{
  time_t now;
  struct tm *tm;
  static char *months[] = {
    "January", "February", "March", "April",
    "May", "June", "July", "August", "September",
    "October", "November", "December"
  };
  static char buf[80];

  time(&now);
  tm = localtime(&now);
  sprintf(buf, "%s %d, 19%d",
	  months[tm->tm_mon], tm->tm_mday, tm->tm_year );
  return buf;
}

- (const char *)contact
{
  return info ? [info contactName] : "";
}

- (const char *)client
{
  return info ? [info clientName] : "";
}

- (const char *)street
{
  return info ? [info street] : "";
}

- (const char *)city
{
  return info ? [info city] : "";
}

- (const char *)state
{
  return info ? [info addrState] : "";
}

- (const char *)zip
{
  return info ? [info zipCode] : "";
}

- (const char *)enddate
{
  int month;
  time_t now;
  struct tm *tm;
  static char buf[80];
  static int lengths[] = {	/* number of days per month */
    31, 28, 31, 30,		/* gloss over leap years... */
    31, 30, 31, 31,
    30, 31, 30, 31
  };

  time(&now);
  tm = localtime(&now);
  month = [self likelyMonth:tm];

  sprintf( buf, "%02d/%02d/%02d", month + 1, lengths[month], tm->tm_year );
  return buf;
}

/*
 * Intuit the most likely start date for this invoice. If it is before
 * the 20th day of the month, assume it is for the prior month, otherwise
 * assume it is for the current month.
 */
- (const char *)startdate
{
  int month;
  time_t now;
  struct tm *tm;
  static char buf[80];

  time(&now);
  tm = localtime(&now);
  month = [self likelyMonth:tm];

  sprintf( buf, "%02d/01/%02d", month + 1, tm->tm_year );
  return buf;
}

- (const char *)billings
{
  static char buf[20];
  
  if ( info ) {
    commafyDouble( [info totalBillable], buf );
    return buf;
  }

  return "";
}

- (const char *)expenses
{
  static char buf[20];
  
  if ( info ) {
    commafyDouble( [info totalExpenses], buf );
    return buf;
  }

  return "";
}

- (const char *)total
{
  static char buf[20];
  
  if ( info ) {
    commafyDouble( [info totalBillable] + [info totalExpenses], buf );
    return buf;
  }

  return "";
}

- (const char *)hours
{
  static char buf[20];
  
  if ( info ) {
    sprintf( buf, "%1.2f", [info totalHours] );
    return buf;
  }
  return "";
}

- (const char *)rate
{
  static char buf[20];
  
  if ( info ) {
    sprintf( buf, "%1.2f", [info hourlyRate] );
    return buf;
  }
  return "";
}

- (const char *)date
{
  if ( session )
    return [session startDateString];

  if ( expense )
    return [expense dateString];

  return "";
}

- (const char *)amount
{
  static char buf[20];

  if ( !expense )
    return "";

  commafyDouble( [expense amount], buf );
  return buf;
}

- (const char *)length
{
  static char buf[20];

  if ( !session )
    return "";

  sprintf( buf, "%1.2f", [session hours] );
  return buf;
}

- (const char *)description
{
  if ( expense )
    return [expense description];

  if ( session )
    return [session description];

  return "";
}

/*
 * Consultant information taken from Preferences
 */
- (const char *)myName
{
  return [[Preferences new] myName];
}

- (const char *)myCompany
{
  return [[Preferences new] myCompany];
}

- (const char *)myStreet
{
  return [[Preferences new] myStreet];
}

- (const char *)myCity
{
  return [[Preferences new] myCity];
}

- (const char *)myState
{
  return [[Preferences new] myState];
}

- (const char *)myZip
{
  return [[Preferences new] myZip];
}

- (const char *)myPhone
{
  return [[Preferences new] myPhone];
}

- (const char *)myFax
{
  return [[Preferences new] myFax];
}

- (const char *)myEmail
{
  return [[Preferences new] myEmail];
}


- (const char *)templatePath:(const char *)template
{
  static char path[FILENAME_MAX + 1];
  sprintf( path, "%s/%sTemplate.%s", templateDir, template, TEMPLATE_TYPE );
  return path;
}

/*
 * If the named template file is not found in the templateDir,
 * copy the one from the app wrapper over there.
 */
- (void)copyTemplateIfNeeded:(const char *)template
{
  FILE *fp;
  const char *templatePath;
  char templateSrc[FILENAME_MAX + 1];
  char name[FILENAME_MAX + 1];

  templatePath = [self templatePath:template];

  /* assume it's ok if we can open it for reading */
  if ( (fp = fopen( templatePath, "r" )) != NULL ) {
    fclose(fp);
    return;			/* the file is already there */
  }

  sprintf( name, "%sTemplate", template );

  if ( ! [[NXBundle mainBundle] getPath:templateSrc
		forResource:name ofType:TEMPLATE_TYPE] ) {
    NXRunAlertPanel( [NXApp name], "Can't find %s.%s in app wrapper",	
		    "What Bogosity!", NULL, NULL, name, TEMPLATE_TYPE );
    return;
  }

  copyFile( templateSrc, templatePath );
}

/*
 * Open a template file for reading.
 */
- (FILE *)openTemplate:(const char *)template
{
  FILE *fp;
  const char *path = [self templatePath:template];

  if ( ! (fp = fopen( path, "r" )) )
    NXRunAlertPanel( [NXApp name], "Can't open `%s' for reading",
		    "Dang!", NULL, NULL, path );

  return fp;
}

/*
 * Take out blanks and punctuation characters to create a reasonable
 * file name.  NB: Returns a pointer to a local static buffer
 */
- (const char *)createFilename:(const char *)type
{
  char c, *ptr;
  const char *dir = GET_DEFAULT( INVOICE_DIR );
  const char *src = [info shortName];
  static char buf[MAXPATHLEN];
  
  sprintf( buf, "%s/", dir ) ;
  ptr = buf + strlen(buf);

  while ( (c = *src++) != '\0' )
    if ( NXIsAlpha(c) )		/* don't copy punctuation */
      *ptr++ = ( NXIsUpper(c) ? NXToLower(c) : c );

  sprintf( ptr, ".%d.%s.%s.rtf", number, [self thisMonth], type );
  return buf;
}

/*
 * Open a file of the given type for writing
 */
- (FILE *)openOutput:(const char *)type
{
  FILE *fp;
  const char *path = [self createFilename:type];

  if ( ! (fp = fopen( path, "w" )) )
    NXRunAlertPanel( [NXApp name], "Can't open `%s' for writing",
		    "Drat.", NULL, NULL, path );

  return fp;
}

/*
 * Read the current "next invoice number" from the defaults database
 * increment and return the number, after storing the value back.
 */
- (int)nextInvoiceNumber
{
  char num[10];
  int value;

  value = atoi(GET_DEFAULT( INVOICE_NUM ));
  sprintf( num, "%d", ++value );
  PUT_DEFAULT( INVOICE_NUM, num );

  return value;			/* return the incremented value */
}

/*
 * Generate an expense report data file for any customer that had
 * expenses for this period.
 */
- generateExpenses
{
  FILE *in, *out;

  if ( !( in = [self openTemplate:EXPENSE]) || !( out = [self openOutput:"exp"] ))
    return nil;

  [self filterExpenses:in to:out];

  fclose( in );
  fclose( out );
 
  return self;
}

- generateDetail
{
  FILE *in, *out;

  if ( !( in = [self openTemplate:DETAIL]) || !( out = [self openOutput:"dtl"] ))
    return nil;

  [self filterDetail:in to:out];

  fclose( in );
  fclose( out );
 
  return self;
}

- generateInvoice
{
  FILE *in, *out;

  if ( !( in = [self openTemplate:INVOICE]) || !( out = [self openOutput:"inv"] ))
    return nil;

  [self filter:in to:out];

  fclose( in );
  fclose( out );
 
  return self;
}

@end
